[非公式] Account Factory for Terraform (AFT) 環境の維持コストを最小化する
どうも、ちゃだいん(@chazuke4649)です。
AFT環境のコストを最小化したい
Account Factory for Terraform (AFT) はControl TowerをTerraformで管理する上でとても便利なソリューションです。 しかし、利用頻度が多い訳ではないワークロードにおいては、その維持コストが気になったりすることがあります。特にPoC/検証環境などなら尚更でしょう。
今回は、非公式な方法ではありますが、AFTの維持コストを最小化してみます。
2024.5.2追記)AFTのversion1.12のアップデートにより、VPC関連リソース利用をOFFできるようになり、そこで当記事のやること1,2が不要になる見込みです。詳細は以下のブログをどうぞ。
[アップデート] Account Factory for Terraform (AFT) でVPCの利用有無とAWS Backup復旧ポイント/S3ログアーカイブバケットの保持期間がカスタマイズ可能に
ちなみに本件は非公式で個人的なワークアラウンドであり、重要度の高い環境では非推奨です。これらによってAFTのシステムやデータが修復不可能になる可能性はゼロではありません。自己責任でお願いします。
前提
- AFT バージョン:
1.6.2
そもそも、VPCエンドポイントの利用は避けよう
コストを抑えたい場合、モジュールのデプロイ時、aft_vpc_endpointsは必ずfalse
にしましょう。
これがデフォルトだとtrue
になり、VPCにVPCエンドポイントが作成され、かなりの課金が発生します。詳細はコチラをどうぞ。
公式的な方法では上記のみであり、それ以上のコスト低減方法は現時点でありません。
よってここからは、非公式的にコスト削減のアクションを実施していきます。
コスト削減効果
※ 金額はあくまで超概算であり、変更される可能性があります。
実施前
- だいたい $4/日 くらい(主に東京リージョン)
よってだいたい $120/月 になります。
実施後
- だいたい $0.4/日 くらい(主に東京リージョン)
よってだいたい $12/月 になります。
やること
- 1-1.NATGWとEIP x2 を削除する
- 1-2.あるLambda x1 を削除する
- 2.別のLambdaの実行頻度を減らす
- (3.DynamoDBストリームにトリガーされたLambdaを外す)
- (4.DynamoDBのグローバルレプリケーションを削除する)
2022.9.30追記)1,2以外はあまりコスト削減効果はない可能性が高いです。追加調査した結果、CloudTrailコストの原因は1つのリージョンに2つ以上の証跡が存在することによる課金でした...(凡ミス)。ですので、1,2以外の3,4は基本的に実施不要でよさそうです。念の為、参考までにオプションとして項目名に()をつけて残しておきます。 "どのリージョンでも管理イベントの最初の証跡は無料ですが、追加の証跡には料金がかかります。組織の証跡の潜在的なコストを削減するには、管理アカウントとメンバーアカウントの不要な証跡を削除することを検討してください"
1-1. NATGWとEIP x2 を削除する
利用料金の中で最も大きな比率を占める、NATGW x2を削除します。
上記が完了後、残ったEIP x2も削除します。
1-2. あるLambda x1を削除する
2024.5.2追記)AFTのversion1.12以降では、対象のLambda関数の名称が変更されたのでご注意ください。
1.12以前: aft-lambda-layer-codebuild-invoker
1.12以降: aft-lambda-layer-codebuild-trigger
NATGWを削除した環境では、Lambda関数aft-lambda-layer-codebuild-invoker
を削除しておかないと、復旧のためのplanがエラーになってしまいます。(以下エラー内容)
╷ │ Error: Lambda function (aft-lambda-layer-codebuild-invoker) returned error: ({"errorMessage": "Connect timeout on endpoint URL: \"https://codebuild.ap-northeast-1.amazonaws.com/\"", "errorType": "ConnectTimeoutError", "stackTrace": [" File \"/var/task/codebuild_invoker.py\", line 28, in lambda_handler\n job_id = client.start_build(projectName=codebuild_project_name)[\"build\"][\"id\"]\n", " File \"/var/runtime/botocore/client.py\", line 391, in _api_call\n return self._make_api_call(operation_name, kwargs)\n", " File \"/var/runtime/botocore/client.py\", line 705, in _make_api_call\n http, parsed_response = self._make_request(\n", " File \"/var/runtime/botocore/client.py\", line 725, in _make_request\n return self._endpoint.make_request(operation_model, request_dict)\n", " File \"/var/runtime/botocore/endpoint.py\", line 104, in make_request\n return self._send_request(request_dict, operation_model)\n", " File \"/var/runtime/botocore/endpoint.py\", line 138, in _send_request\n while self._needs_retry(attempts, operation_model, request_dict,\n", " File \"/var/runtime/botocore/endpoint.py\", line 254, in _needs_retry\n responses = self._event_emitter.emit(\n", " File \"/var/runtime/botocore/hooks.py\", line 357, in emit\n return self._emitter.emit(aliased_event_name, **kwargs)\n", " File \"/var/runtime/botocore/hooks.py\", line 228, in emit\n return self._emit(event_name, kwargs)\n", " File \"/var/runtime/botocore/hooks.py\", line 211, in _emit\n response = handler(**kwargs)\n", " File \"/var/runtime/botocore/retryhandler.py\", line 183, in __call__\n if self._checker(attempts, response, caught_exception):\n", " File \"/var/runtime/botocore/retryhandler.py\", line 250, in __call__\n should_retry = self._should_retry(attempt_number, response,\n", " File \"/var/runtime/botocore/retryhandler.py\", line 277, in _should_retry\n return self._checker(attempt_number, response, caught_exception)\n", " File \"/var/runtime/botocore/retryhandler.py\", line 316, in __call__\n checker_response = checker(attempt_number, response,\n", " File \"/var/runtime/botocore/retryhandler.py\", line 222, in __call__\n return self._check_caught_exception(\n", " File \"/var/runtime/botocore/retryhandler.py\", line 359, in _check_caught_exception\n raise caught_exception\n", " File \"/var/runtime/botocore/endpoint.py\", line 201, in _do_get_response\n http_response = self._send(request)\n", " File \"/var/runtime/botocore/endpoint.py\", line 270, in _send\n return self.http_session.send(request)\n", " File \"/var/runtime/botocore/httpsession.py\", line 442, in send\n raise ConnectTimeoutError(endpoint_url=request.url, error=e)\n"]}) │ │ with module.aft.module.aft_lambda_layer.data.aws_lambda_invocation.invoke_codebuild_job, │ on .terraform/modules/aft/modules/aft-lambda-layer/lambda.tf line 21, in data "aws_lambda_invocation" "invoke_codebuild_job": │ 21: data "aws_lambda_invocation" "invoke_codebuild_job" { │ ╵
planが通るようにするため、Lambda関数aft-lambda-layer-codebuild-invoker
を削除します。
2. 別のLambdaの実行頻度を減らす
Lambda関数aft-account-request-processor
は、EventBridgeaft-lambda-account-request-processor
によって、5分ごとに実行される設定になっています。これによって Lambdaのコストがかかる可能性があるので、この頻度を下げます。
対象のEventBridgeはLambdaのコンソールにて見つけることができます。
EventBridgeルールの設定にて、5分
ごとを30日
ごとに変更します。
(3. DynamoDBストリームにトリガーされたLambdaを外す)
DynamoDBテーブルaft-request
はストリームが有効化されており、トリガーとしてLambdaaft-account-request-action-trigger
とaft-account-request-audit-trigger
が設定されています。
これによりLambdaが高頻度でDybamoDBテーブルのストリームをポーリングするため、CloudTrailのコストがかさみます。
よって2つ分のLambdaのトリガー設定を外します。
下にスクロールすると、トリガー設定があるのでこれ2つを削除します。
(4. DynamoDBのグローバルレプリケーションを削除する)
DynamoDBテーブルaft-backend-{AwsAccountId}
は高可用性のためグローバルレプリケーションによってシンガポールにテーブルがレプリケートされています。
微々たるではありますが、今回はコスト削減を徹底するため、レプリケーション先のテーブルを削除します。
コスト削減活動は以上です。
復旧方法
もちろんこのままではAFTは正常に機能しません。実際にAFTを使用する機会が訪れたとして、AFTが機能するように復旧させる必要があります。
やり方は簡単で、AFTモジュールを再度適用し、手動削除・変更したリソースを修復します。
ここはIaCの強みを活かし、コードで定義した状態に戻そうとする動きを活用します。
管理アカウント権限で terraform plan / apply
を実行します。
% terraform plan ## 中略 ## ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create ~ update in-place <= read (data resources) Terraform will perform the following actions: # module.aft.module.aft_account_request_framework.aws_cloudwatch_event_rule.aft_account_request_processor will be updated in-place ~ resource "aws_cloudwatch_event_rule" "aft_account_request_processor" { id = "aft-lambda-account-request-processor" name = "aft-lambda-account-request-processor" ~ schedule_expression = "rate(30 days)" -> "rate(5 minutes)" tags = {} # (5 unchanged attributes hidden) } # module.aft.module.aft_account_request_framework.aws_eip.aft-vpc-natgw-01 will be created + resource "aws_eip" "aft-vpc-natgw-01" { + allocation_id = (known after apply) + association_id = (known after apply) + carrier_ip = (known after apply) + customer_owned_ip = (known after apply) + domain = (known after apply) + id = (known after apply) + instance = (known after apply) + network_border_group = (known after apply) + network_interface = (known after apply) + private_dns = (known after apply) + private_ip = (known after apply) + public_dns = (known after apply) + public_ip = (known after apply) + public_ipv4_pool = (known after apply) + tags_all = { + "managed_by" = "AFT" } + vpc = (known after apply) } # module.aft.module.aft_account_request_framework.aws_eip.aft-vpc-natgw-02 will be created + resource "aws_eip" "aft-vpc-natgw-02" { + allocation_id = (known after apply) + association_id = (known after apply) + carrier_ip = (known after apply) + customer_owned_ip = (known after apply) + domain = (known after apply) + id = (known after apply) + instance = (known after apply) + network_border_group = (known after apply) + network_interface = (known after apply) + private_dns = (known after apply) + private_ip = (known after apply) + public_dns = (known after apply) + public_ip = (known after apply) + public_ipv4_pool = (known after apply) + tags_all = { + "managed_by" = "AFT" } + vpc = (known after apply) } # module.aft.module.aft_account_request_framework.aws_lambda_event_source_mapping.aft_account_request_action_trigger will be created + resource "aws_lambda_event_source_mapping" "aft_account_request_action_trigger" { + batch_size = 1 + enabled = true + event_source_arn = "arn:aws:dynamodb:ap-northeast-1:111111111111:table/aft-request/stream/2021-11-30T08:05:19.518" + function_arn = (known after apply) + function_name = "arn:aws:lambda:ap-northeast-1:111111111111:function:aft-account-request-action-trigger" + id = (known after apply) + last_modified = (known after apply) + last_processing_result = (known after apply) + maximum_record_age_in_seconds = (known after apply) + maximum_retry_attempts = 1 + parallelization_factor = (known after apply) + starting_position = "LATEST" + state = (known after apply) + state_transition_reason = (known after apply) + uuid = (known after apply) } # module.aft.module.aft_account_request_framework.aws_lambda_event_source_mapping.aft_account_request_audit_trigger will be created + resource "aws_lambda_event_source_mapping" "aft_account_request_audit_trigger" { + batch_size = 1 + enabled = true + event_source_arn = "arn:aws:dynamodb:ap-northeast-1:111111111111:table/aft-request/stream/2021-11-30T08:05:19.518" + function_arn = (known after apply) + function_name = "arn:aws:lambda:ap-northeast-1:111111111111:function:aft-account-request-audit-trigger" + id = (known after apply) + last_modified = (known after apply) + last_processing_result = (known after apply) + maximum_record_age_in_seconds = (known after apply) + maximum_retry_attempts = 1 + parallelization_factor = (known after apply) + starting_position = "LATEST" + state = (known after apply) + state_transition_reason = (known after apply) + uuid = (known after apply) } # module.aft.module.aft_account_request_framework.aws_nat_gateway.aft-vpc-natgw-01 will be created + resource "aws_nat_gateway" "aft-vpc-natgw-01" { + allocation_id = (known after apply) + connectivity_type = "public" + id = (known after apply) + network_interface_id = (known after apply) + private_ip = (known after apply) + public_ip = (known after apply) + subnet_id = "subnet-0fd5be2f3d40b1e8b" + tags = { + "Name" = "aft-vpc-natgw-01" } + tags_all = { + "Name" = "aft-vpc-natgw-01" + "managed_by" = "AFT" } } # module.aft.module.aft_account_request_framework.aws_nat_gateway.aft-vpc-natgw-02 will be created + resource "aws_nat_gateway" "aft-vpc-natgw-02" { + allocation_id = (known after apply) + connectivity_type = "public" + id = (known after apply) + network_interface_id = (known after apply) + private_ip = (known after apply) + public_ip = (known after apply) + subnet_id = "subnet-050a3231338de4f45" + tags = { + "Name" = "aft-vpc-natgw-02" } + tags_all = { + "Name" = "aft-vpc-natgw-02" + "managed_by" = "AFT" } } # module.aft.module.aft_account_request_framework.aws_route_table.aft_vpc_private_subnet_01 will be updated in-place ~ resource "aws_route_table" "aft_vpc_private_subnet_01" { id = "rtb-0ee906de3470c5157" ~ route = [ - { - carrier_gateway_id = "" - cidr_block = "0.0.0.0/0" - core_network_arn = "" - destination_prefix_list_id = "" - egress_only_gateway_id = "" - gateway_id = "" - instance_id = "" - ipv6_cidr_block = "" - local_gateway_id = "" - nat_gateway_id = "nat-0efbd5f8afc83ba59" - network_interface_id = "" - transit_gateway_id = "" - vpc_endpoint_id = "" - vpc_peering_connection_id = "" }, # (1 unchanged element hidden) ] tags = { "Name" = "aft-vpc-private-subnet-01" } # (5 unchanged attributes hidden) } # module.aft.module.aft_account_request_framework.aws_route_table.aft_vpc_private_subnet_02 will be updated in-place ~ resource "aws_route_table" "aft_vpc_private_subnet_02" { id = "rtb-0dc79631f7a2d3880" ~ route = [ - { - carrier_gateway_id = "" - cidr_block = "0.0.0.0/0" - core_network_arn = "" - destination_prefix_list_id = "" - egress_only_gateway_id = "" - gateway_id = "" - instance_id = "" - ipv6_cidr_block = "" - local_gateway_id = "" - nat_gateway_id = "nat-0d3af787ce7388ff2" - network_interface_id = "" - transit_gateway_id = "" - vpc_endpoint_id = "" - vpc_peering_connection_id = "" }, # (1 unchanged element hidden) ] tags = { "Name" = "aft-vpc-private-subnet-02" } # (5 unchanged attributes hidden) } # module.aft.module.aft_backend.aws_dynamodb_table.lock-table will be updated in-place ~ resource "aws_dynamodb_table" "lock-table" { id = "aft-backend-111111111111" name = "aft-backend-111111111111" tags = { "Name" = "aft-backend-111111111111" } # (10 unchanged attributes hidden) + replica { + kms_key_arn = (known after apply) + point_in_time_recovery = false + propagate_tags = false + region_name = "ap-southeast-1" } # (3 unchanged blocks hidden) } # module.aft.module.aft_lambda_layer.data.aws_lambda_invocation.invoke_codebuild_job will be read during apply # (config refers to values not yet known) <= data "aws_lambda_invocation" "invoke_codebuild_job" { ~ id = "aft-lambda-layer-codebuild-invoker_$LATEST_3d56455871df474e76c910b73e2d2648" -> (known after apply) - qualifier = "$LATEST" -> null ~ result = jsonencode( { - Status = 200 } ) -> (known after apply) # (2 unchanged attributes hidden) } # module.aft.module.aft_lambda_layer.aws_lambda_function.codebuild_invoker will be created + resource "aws_lambda_function" "codebuild_invoker" { + architectures = (known after apply) + arn = (known after apply) + description = "AFT Lambda Layer - CodeBuild Invoker" + filename = ".terraform/modules/aft/modules/aft-archives/../../src/aft_lambda/aft_builder.zip" + function_name = "aft-lambda-layer-codebuild-invoker" + handler = "codebuild_invoker.lambda_handler" + id = (known after apply) + invoke_arn = (known after apply) + last_modified = (known after apply) + memory_size = 1024 + package_type = "Zip" + publish = false + qualified_arn = (known after apply) + reserved_concurrent_executions = -1 + role = "arn:aws:iam::111111111111:role/codebuild_invoker_role" + runtime = "python3.8" + signing_job_arn = (known after apply) + signing_profile_version_arn = (known after apply) + source_code_hash = "MQZ8MM29zszuIJTotrdcCzRxdrLwMk6gh/6VjrdOcLI=" + source_code_size = (known after apply) + tags_all = { + "managed_by" = "AFT" } + timeout = 900 + version = (known after apply) + ephemeral_storage { + size = (known after apply) } + tracing_config { + mode = (known after apply) } + vpc_config { + security_group_ids = [ + "sg-026ad8cf6c06c5063", ] + subnet_ids = [ + "subnet-0b367c1cc182804fc", + "subnet-0dca45d7cc47d0f86", ] + vpc_id = (known after apply) } } Plan: 7 to add, 4 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
※AWSアカウントIDはダミー値(111111111111)に変更しています
問題なければ、適用します。
% terraform apply -auto-approve ## 中略 ## module.aft.module.aft_backend.aws_dynamodb_table.lock-table: Still modifying... [id=aft-backend-597692810508, 11m10s elapsed] module.aft.module.aft_backend.aws_dynamodb_table.lock-table: Still modifying... [id=aft-backend-597692810508, 11m20s elapsed] module.aft.module.aft_backend.aws_dynamodb_table.lock-table: Still modifying... [id=aft-backend-597692810508, 11m30s elapsed] module.aft.module.aft_backend.aws_dynamodb_table.lock-table: Modifications complete after 11m31s [id=aft-backend-111111111111] Apply complete! Resources: 7 added, 4 changed, 0 destroyed.
削除・変更したリソースが修復されました。
終わりに
AFT環境の維持コストを最小化してみました。
それでは今日はこの辺で。ちゃだいん(@chazuke4649)でした。